/*
 * Módulo que implementa el algoritmo MD5 para calcular el hash
 * de un archivo.
 * 
 * 
 * 90365 - Nahuel M. Lucero Seinturia
 * 
 */

#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdbool.h>
#include "common_md5.h"

enum REGISTROS{H0, H1, H2, H3};
enum REGISTROS_TEMP{A, B, C, D};

/*
 * Funciones privadas utilizadas en el calculo del hash: f,g,h e i.
 */ 
unsigned int F(const unsigned int x, const unsigned int y, \
			   const unsigned int z){
	return ((x & y) | (~x & z));
}

unsigned int G(const unsigned int x, const unsigned int y, \
			   const unsigned int z){
	return ((x & z) | (y & ~z));
}

unsigned int H(const unsigned int x, const unsigned int y, \
			   const unsigned int z){
	return (x ^ y ^ z);
}
unsigned int I(const unsigned int x, const unsigned int y, \
			   const unsigned int z){
	return (y ^ (x | ~z));
}

unsigned int leftRotate(const unsigned int x, const unsigned int c){
	return ((x << c) | (x >> (32-c)));
}

/* Procesa un bloque de 64 bytes, realizando las operaciones necesarias
 * y actualizando los valores actuales del hash.
 */
void procesarBloque(unsigned char* bloque, unsigned int* vectorHash,
					const unsigned int* constantesRotacion,
					const unsigned int* tablaConstantes){
						
	int i;
	unsigned int funcionAplicada;
	unsigned int posicion;
	unsigned int temp;
	unsigned int sumaTemp;	
						
	/* 'Vemos' al bloque como un vector de 16 enteros sin signo de 4
	 * bytes (little endian)
	 */	
	unsigned int* enterosLE = (unsigned int*) bloque;
	
	/* Vector que contendra valores temporales del hash a utilizar en 
	 * los calculos
	 */
	unsigned int vectorTemp[4];		
		
	/* Procesamos el bloque de 512 bits */
	vectorTemp[A] = vectorHash[H0];
	vectorTemp[B] = vectorHash[H1];
	vectorTemp[C] = vectorHash[H2];
	vectorTemp[D] = vectorHash[H3];
	
	for(i = 0; i < 64; i++){
		if (i <= 15){
			funcionAplicada = F(vectorTemp[B], vectorTemp[C], \
								vectorTemp[D]);
			posicion = i;				
		}else if ((i >= 16) && (i <= 31)){
			funcionAplicada = G(vectorTemp[B], vectorTemp[C], \
								vectorTemp[D]);
			posicion = (5*i + 1) % 16;
		}else if ((i >= 32) && (i <= 47)){
			funcionAplicada = H(vectorTemp[B], vectorTemp[C], \
								vectorTemp[D]);
			posicion = (3*i + 5) % 16;
		}else{
			funcionAplicada = I(vectorTemp[B], vectorTemp[C], \
								vectorTemp[D]);
			posicion = (7*i) % 16;
		}
		
		/* Actualizamos los valores del vector temporal del hash */
		sumaTemp = vectorTemp[A] + funcionAplicada + tablaConstantes[i] \
				   + *(enterosLE+posicion);			
		temp = vectorTemp[D];
		
		vectorTemp[D] = vectorTemp[C];
		vectorTemp[C] = vectorTemp[B];
		vectorTemp[B] += leftRotate(sumaTemp, constantesRotacion[i]);
		vectorTemp[A] = temp;			
	}
	
	/* Actualizamos los valores del hash */
	vectorHash[H0] += vectorTemp[A];
	vectorHash[H1] += vectorTemp[B];
	vectorHash[H2] += vectorTemp[C];
	vectorHash[H3] += vectorTemp[D];	
}

/* Escribe la longitud original del archivo al final del bloque,
 * como un entero sin signo de 64 bits en little endian
 */
void agregarLongitud(char* bloque, const unsigned int* cantidadDeBits){
    unsigned int i;
    
    for(i = 56; i < 59; i++) {
        bloque[i] = (unsigned char) \
                    (((cantidadDeBits[0] >> (8*(i-56)))) & 0xff);
    }
    
    for(i = 60; i < 64; i++) {
        bloque[i] = (unsigned char) \
                    (((cantidadDeBits[1] >> (8*(i-60)))) & 0xff);
    }
}

/* Funcion principal */
void calcularHashMD5(char* nombreArchivo, char* hashMD5calculado){
	FILE* archivoAProcesar;
	
	/* Si no se puede abrir el archivo, devuelve una cadena vacia */
	if(!(archivoAProcesar = fopen(nombreArchivo, "rb"))){
		hashMD5calculado[0] = '\0';
		return;
	}
	
	/* Vector que contendra los caracteres del hash MD5 en el orden
	 * correcto
	 */
	unsigned char hashMD5char[16];	
	
	/* Tabla de constantes para utilizar en el calculo del hash */
	unsigned int tablaConstantes[64];
	
	/* Inicializamos la tabla de constantes */
	int i;
	for(i = 0; i < 64; i++){
		tablaConstantes[i] = (unsigned int)floor(fabs(sin(i+1)) * \
												 pow(2,32));
	}
	
	/* Constantes que se utilizaran en las llamadas a leftRotate */
	unsigned int constantesRotacion[64] = 
	{7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
	 5,  9, 14, 20, 5,  9, 14, 20, 5,  9, 14, 20, 5,  9, 14, 20,
	 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
	 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};

	/* Vector que contendra los valores del hash, con sus valores 
	 * iniciales 
	 */
	unsigned int vectorHash[4];		
	vectorHash[H0] = 0x67452301;
	vectorHash[H1] = 0xefcdab89;
	vectorHash[H2] = 0x98badcfe;
	vectorHash[H3] = 0x10325476;	
	
	/* Buffer donde se iran guardando los bloques de 512 bits que se 
	 * iran leyendo del archivo. En caso de que el ultimo bloque tenga 
	 * mas de 55 bytes, al hacer el padding se usara un bloque adicional
	 */
	unsigned char bufferArchivo[64];
	unsigned char bloqueAdicional[64] = {
	  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
	  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
	  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
	};	
	
	/* Contadores de bits/bytes leidos. Los bits menos significativos 
	 * estan en la primera posicion
	 */
	unsigned int contadorDeBits[2] = {0,0};	
	unsigned int bytesLeidos;
	unsigned int contadorBytesLeidos = 0;
	unsigned int contadorBytesLeidosAnterior = 0;	
	unsigned int ultimosBytesLeidos = 0;	
	unsigned char aux[3];
	bool hayBloqueAdicional = false;
		
	fprintf(stderr, "%s - comienza procesamiento\n", nombreArchivo);
		
	/* Vamos leyendo de a bloques de 512 bits (64 bytes) */
	while((bytesLeidos = fread(bufferArchivo, 1, 64, archivoAProcesar))\
		   != 0){
		contadorBytesLeidosAnterior = contadorBytesLeidos;
		contadorBytesLeidos += bytesLeidos;
		ultimosBytesLeidos = bytesLeidos;		
		
		/* Sumamos los bits leidos */
		if ((contadorDeBits[0] += (bytesLeidos*8)) < (bytesLeidos*8)){
			contadorDeBits[1]++;
		}		
				
		/* Hacemos el padding correspondiente */
		for(i = bytesLeidos; i < 64; i++){
			bufferArchivo[i] = 0x0;
		}				
		
		if(bytesLeidos < 64){
			bufferArchivo[bytesLeidos] = 0x80;
			/* Agregamos la longitud en bits del mensaje original en los
			 * ultimos 64 bits del bloque correspondiente
			 */
			if(bytesLeidos < 56){
				/* Se agrega la longitud al final del bloque original */
				agregarLongitud(bufferArchivo, contadorDeBits);
			}else{
				hayBloqueAdicional = true;
				/* Se agrega la longitud al final del bloque adicional*/
				agregarLongitud(bloqueAdicional, contadorDeBits);								
			}
		}
		
		procesarBloque(bufferArchivo, vectorHash, constantesRotacion, \
					   tablaConstantes);		
		
		/* Si se proceso un kilobyte mas, se informa por stderr */
		if ((contadorBytesLeidosAnterior & 0x400) != \
		   ((contadorBytesLeidos & 0x400))){
			fprintf(stderr, "%s - kilobyte procesado\n", nombreArchivo);
		}
		
		/* Si hay bloque adicional, lo procesamos como ultimo paso */
		if(hayBloqueAdicional){
			procesarBloque(bloqueAdicional, vectorHash, \
				           constantesRotacion, tablaConstantes);
		}
		
	}	
	
	/* Casos particulares:
	 * 
	 * Si en la ultima lectura se leyeron 64 bytes exactos, hay que 
	 * hacer el padding igual
	 */
	if(ultimosBytesLeidos == 64){
		bloqueAdicional[0] = 0x80;
		/* Agregamos la longitud en bits del mensaje original en los
		 * ultimos 64 bits del bloque adicional
		 */
		agregarLongitud(bloqueAdicional, contadorDeBits);		
		
		/* Procesamos el bloque adicional */
		procesarBloque(bloqueAdicional, vectorHash, constantesRotacion,\
					   tablaConstantes);	
	}
	
	/* Si el archivo es vacio (0 bits), hay que hacer el padding y 
	 * procesar el bloque
	 */
	if((contadorBytesLeidos == 0)){
		bufferArchivo[0] = 0x80;
		
		for(i = 1; i < 64; i++){
			bufferArchivo[i] = 0x0;
		}
		
		procesarBloque(bufferArchivo, vectorHash, constantesRotacion, \
					   tablaConstantes);			
	}
	
	/* Pasamos el hash obtenido a una cadena de caracteres hexadecimales 
	 */    
	for(i = 0; i < 4; i++) {
        int j;        
		for(j = 0; j < 4; j++) {
			hashMD5char[4*i+j] = (unsigned char) \
								 (((vectorHash[i] >> (8*j))) & 0xff);
		}		
	}		
	aux[0] = '\0';
	hashMD5calculado[0] = '\0';
	for(i = 0; i < 16; i++){
		sprintf((char*)aux, "%02x", (unsigned int) hashMD5char[i]);
		strncat(hashMD5calculado, (char*)aux, 2);
	}	
	
	fprintf(stderr, "%s - finaliza procesamiento\n", nombreArchivo);
	fclose(archivoAProcesar);
		
}
